﻿namespace TomatoBreak.Infrastructure
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Linq;
    using System.Reflection;

    using ExtensionMethods;

    public class TypeQuery : IEnumerable<Type>
    {
        public TypeQuery() : this(new List<Assembly>(), new List<Predicate<Type>>(), new List<Predicate<Type>>())
        {
        }

        protected TypeQuery(ICollection<Assembly> assemblies, ICollection<Predicate<Type>> includes, ICollection<Predicate<Type>> excludes)
        {
            if (assemblies == null)
            {
                throw new ArgumentNullException("assemblies");
            }

            if (includes == null)
            {
                throw new ArgumentNullException("includes");
            }

            if (excludes == null)
            {
                throw new ArgumentNullException("excludes");
            }

            Assemblies = assemblies;
            Includes = includes;
            Excludes = excludes;
        }

        protected ICollection<Assembly> Assemblies { get; private set; }

        protected ICollection<Predicate<Type>> Includes { get; private set; }

        protected ICollection<Predicate<Type>> Excludes { get; private set; }

        public TypeQuery Add(params Assembly[] assemblies)
        {
            if (assemblies.Any())
            {
                foreach (var assembly in assemblies.Where(assembly => !Assemblies.Contains(assembly)))
                {
                    Assemblies.Add(assembly);
                }
            }

            return this;
        }

        public TypeQuery Include(params Type[] types)
        {
            if (types.Any())
            {
                types.Each(type => Include(scannedType => scannedType == type));
            }

            return this;
        }

        public TypeQuery Include(Predicate<Type> filter)
        {
            Includes.Add(filter);

            return this;
        }

        public TypeQuery Exclude(params Type[] types)
        {
            if (types.Any())
            {
                types.Each(type => Exclude(scannedType => scannedType == type));
            }

            return this;
        }

        public TypeQuery Exclude(Predicate<Type> filter)
        {
            Excludes.Add(filter);

            return this;
        }

        public IEnumerator<Type> GetEnumerator()
        {
            var filteredTypes = Assemblies.SelectMany(assembly => assembly.GetExportedTypes());

            if (Includes.Any())
            {
                filteredTypes = filteredTypes.Where(type => Includes.Any(filter => filter(type)));
            }

            return filteredTypes.Where(type => !Excludes.Any(filter => filter(type))).GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }
}